﻿/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A
 * copy of the license can be found at http://go.microsoft.com/fwlink/?LinkID=131993. If
 * you cannot locate the Microsoft Public License, please send an email to
 * mixon@microsoft.com. By using this source code in any fashion, you are agreeing to
 * be bound by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Json;
using System.Linq;
using System.Windows.Browser;

namespace MIXOnline.Descry
{
    /// <summary>
    /// This class encapsulates as much of the FriendFeed data access logic as possible. The page.xaml.cs file contains 
    /// some FriendFeed specific logic, but we've tried to mimimize that as much as possible to keep the design as generic as possible.
    /// </summary>
    public class FriendFeedDataModule
    {
        public FriendFeedDataModule()
        {
            InitModule();

            this.OnStatusChanged(new TimelineEventArgs("FriendFeedModule Loaded."));
        }

        #region BackgroundWorkers

        void _loadCacheWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            // Try to get cached data from Isolated Storage
            IEnumerable<Entry> cached = Storage.ReadObjectFromStorage<IEnumerable<Entry>>(GenerateFileName());
            if (cached == null)
                _entryCache = new List<Entry>(); //set to empty list if cache was empty
            else
                _entryCache = cached.ToList();

            _loadCacheWorker.ReportProgress(0, e.Argument);
        }

        void _loadCacheWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            _origRecordCount = _entryCache.Count(); // how many entries are there anyway?

            this.OnEntriesChange(new TimelineEventArgs(null)); // fire event so page knows to update UI 
            this.OnStatusChanged(new TimelineEventArgs("Displaying " + _origRecordCount.ToString() + " of " + _origRecordCount.ToString() + " posts"));

            // get new data
            if ((bool)e.UserState) BeginGetData(this._userName);
        }

        public List<Service> EnumerateServices()
        {
            return (from p in this.UnfilteredItems
                    orderby p.Service.Name
                    select p.Service).Distinct(new ServiceEqualityComparer()).ToList();
        }

        public List<User> EnumerateUsers()
        {
            return (from p in this.UnfilteredItems
                    orderby p.User.Name
                    select p.User).Distinct(new UserEqualityComparer()).ToList();
        }

        void _saveCacheWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            Storage.WriteObjectToStorage<IEnumerable<Entry>>(_entryCache, GenerateFileName());
            _saveCacheWorker.ReportProgress(0);
        }

        void _saveCacheWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            this.OnEntriesChange(new TimelineEventArgs(null)); // fire event so page knows to update
            int newcount = _entryCache.Count();
            this.OnStatusChanged(new TimelineEventArgs("Displaying " + newcount.ToString() + " of " + newcount.ToString() + " posts"));
        }

        #endregion BackgroundWorkers

        #region Methods

        /// <summary>
        /// Initialize backgroundworkers and register ScriptableObject
        /// </summary>
        private void InitModule()
        {
            HtmlPage.RegisterScriptableObject("DataModule", this); // So that the HTML DOM will have access to the instance of this class

            // Background Thread setup
            _loadCacheWorker.WorkerReportsProgress = true;
            _loadCacheWorker.ProgressChanged += new ProgressChangedEventHandler(_loadCacheWorker_ProgressChanged);
            _loadCacheWorker.DoWork += new DoWorkEventHandler(this._loadCacheWorker_DoWork);

            _saveCacheWorker.WorkerReportsProgress = true;
            _saveCacheWorker.ProgressChanged += new ProgressChangedEventHandler(_saveCacheWorker_ProgressChanged);
            _saveCacheWorker.DoWork += new DoWorkEventHandler(_saveCacheWorker_DoWork);
        }

        #region JScript Calls

        /// <summary>
        /// Determine which type of FriendFeed request we need to make
        /// </summary>
        /// <param name="username"></param>
        /// <param name="page"></param>
        private void GetDataPage(string username, int page)
        {
            int startPosition = page * PAGE_SIZE;

            string endPosition = (startPosition + PAGE_SIZE).ToString();

            if (_getFriends)
            {
                if (username.Contains(",")) // if multiple users
                {
                    // this case does not work.
                    this.OnStatusChanged(new TimelineEventArgs("ERROR: FriendFeed does not support getting friends of multiple nicknames."));
                }
                else
                    InvokeJScript(string.Format(FRIENDFEED_USER_AND_FRIENDS_REQUEST, username, startPosition, endPosition), endPosition);
            }
            else
            {
                if (username.Contains(",")) // if multiple users
                    InvokeJScript(string.Format(FRIENDFEED_MULTIPLE_USERS_REQUEST, username, startPosition, endPosition), endPosition);
                else
                    InvokeJScript(string.Format(FRIENDFEED_USER_REQUEST, username, startPosition, endPosition), endPosition);
            }
        }

        /// <summary>
        /// Actually make a call the the HTML DOM to make a Script call to get a JSON object!
        /// </summary>
        /// <param name="url"></param>
        /// <param name="qty"></param>
        private void InvokeJScript(string url, string qty)
        {
            this.OnStatusChanged(new TimelineEventArgs("Downloading FF... " + qty));
            HtmlPage.Window.Invoke("injectScript", url); // Call to HTML DOM
        }

        /// <summary>
        /// A JScript callback that passes in a JSON string
        /// </summary>
        /// <param name="data"></param>
        [ScriptableMember]
        public void PassData(string data)
        {
            // merge it.
            bool stillMoreData = MergeData((JsonObject)JsonObject.Parse(data));

            // sort it.
            _entryCache = (from p in _entryCache orderby p.Published descending select p).ToList();

            // is there new content after union?
            if (stillMoreData && _currentPage < _maxPageCount)
            {
                this.OnEntriesChange(new TimelineEventArgs(null));
                _currentPage++; // if different, get more data
                GetDataPage(_userName, _currentPage); // fire off the next JScript call via HTML DOM
            }
            else // No new entries found on FriendFeed or the maxPageCount value has been exceeded
            {
                // cache it. 
                if (_origRecordCount != _entryCache.Count()) // only if it changed
                {
                    if (!_saveCacheWorker.IsBusy) // is it already running?
                    {
                        this.OnStatusChanged(new TimelineEventArgs("Updating cache..."));
                        _saveCacheWorker.RunWorkerAsync();
                    }
                }
                this.OnStatusChanged(new TimelineEventArgs("Displaying " + _entryCache.Count().ToString() + " of " + _entryCache.Count().ToString() + " posts"));
            }
        }

        #endregion JScript Calls

        /// <summary>
        /// Entry function. the GetDataPage function can be thought of as an async call, because we don't
        /// know when the HTML DOM will return the data in the PassData method.
        /// </summary>
        /// <param name="username"></param>
        public void BeginGetData(string username)
        {
            _currentPage = 0; //reset
            this._userName = username;
            this.GetDataPage(username, _currentPage);
        }

        public string GenerateFileName()
        {
            return _userName.Replace("'", "") + "." + FILENAME_SUFFIX;
        }

        public void LoadData(string name, bool updateFriendFeed)
        {
            _userName = name; // localize name for later use

            this.OnStatusChanged(new TimelineEventArgs("Loading " + name + " data from cache..."));

            // load data from IsolatedStorage cache asynchronously
            if (!_loadCacheWorker.IsBusy)
                _loadCacheWorker.RunWorkerAsync(updateFriendFeed); 
        }

        /// <summary>
        /// This is a very cool and elegant function which merges new data with old data and ignores 
        /// data that it already contains and localizes it. Even cooler it takes a JSON object... 
        /// it could have just as well been an XElement instead.
        /// </summary>
        /// <param name="jo"></param>
        /// <returns>Whether it has gotten new data or not</returns>
        private bool MergeData(JsonObject jo)
        {
            IEnumerable<Entry> newEntries = from res in (JsonArray)jo["entries"] // project JSON object into IEnumberable<Entry>
                                            select new Entry()
                                            {
                                                Title = res["title"],
                                                Updated = res["updated"],
                                                Anonymous = res["anonymous"],
                                                Hidden = res["hidden"],
                                                Published = DateTime.Parse(res["published"]),
                                                Id = new Guid(res["id"]),
                                                Link = res["link"],
                                                Service = new Service()
                                                {
                                                    IconUrl = res["service"]["iconUrl"],
                                                    Id = res["service"]["id"],
                                                    Name = res["service"]["name"],
                                                    ProfileUrl = res["service"]["profileUrl"],
                                                    IsChecked = true,
                                                },
                                                User = new User()
                                                {
                                                    Id = new Guid(res["user"]["id"]),
                                                    Name = res["user"]["name"],
                                                    NickName = res["user"]["nickname"],
                                                    ProfileUrl = res["user"]["profileUrl"],
                                                    IsChecked = true,
                                                },

                                                Media =
                                                    (from JsonValue m in res["media"]
                                                     select
                                                         new Media
                                                         {
                                                             Link = new Uri(m.ValueOr("link", "")),
                                                             Title = m.ValueOr("title", ""),
                                                             Player = m.ValueOr("player", null),
                                                             Content =
                                                             (from JsonValue c in m["content"].ToListOrEmpty()
                                                              select
                                                                  new MediaItem
                                                                  {
                                                                      Width = c.ValueOr("width", 0),
                                                                      Height = c.ValueOr("height", 0),
                                                                      Type = c.ValueOr("type", null),
                                                                      Url = c.ValueOr("url", null)
                                                                  }).ToList(),
                                                             Thumbnails =
                                                            (from JsonValue c in m["thumbnails"].ToListOrEmpty()
                                                             select
                                                                 new MediaItem
                                                                 {
                                                                     Width = c.ValueOr("width", 0),
                                                                     Height = c.ValueOr("height", 0),
                                                                     Url = c.ValueOr("url", null)
                                                                 }).ToList(),
                                                             Enclosures =
                                                            (from JsonValue c in m["enclosures"].ToListOrEmpty()
                                                             select
                                                                 new MediaItem
                                                                 {
                                                                     Url = c.ValueOr("url", null),
                                                                     Type = c.ValueOr("type", null)
                                                                 }).ToList(),

                                                         }).ToList(),

                                                Likes =
                                                    (from JsonValue like in res["likes"] select like).Count(),
                                                Comments =
                                                    (from JsonValue comment in res["comments"] select comment).Count()
                                            };

            int oldEntryCount = _entryCache.Count();
            _entryCache = (_entryCache.Union(newEntries, new EntryEqualityComparer())).ToList(); // append only new Entry items
            int newEntryCount = _entryCache.Count();

            return (oldEntryCount != newEntryCount);
        }

        public void DeleteCacheFile(string nickname)
        {
            this._userName = nickname;
            Storage.DeleteFile(GenerateFileName());
        }

        #endregion Methods

        #region Events

        public event SurfaceEventHandler StatusChanged;

        protected virtual void OnStatusChanged(TimelineEventArgs e)
        {
            SurfaceEventHandler handler = this.StatusChanged;
            if (handler != null)
                handler(this, e);
        }

        public event SurfaceEventHandler ItemsChanged;

        protected virtual void OnEntriesChange(TimelineEventArgs e)
        {
            SurfaceEventHandler handler = this.ItemsChanged;
            if (handler != null)
                handler(this, e);
        }

        #endregion Events

        #region Filters

        /*
         * These simple filters are FriendFeed specific and exposed publicly so that the Page.xaml.cs can get to them
         * based on UI decisions the user makes
         */

        public List<Entry> FilterEntryByUsers(List<Entry> entry_items, List<User> users)
        {
            // return a list of selected users
            List<Entry> user_filteredentryitems = (from p in entry_items as IEnumerable<Entry>
                                                   join u in users on p.User.Id equals u.Id
                                                   select p).ToList();
            return user_filteredentryitems;
        }

        public List<Entry> FilterEntryByServices(List<Entry> entry_items, List<Service> services)
        {
            // return a list of selected services
            List<Entry> service_filteredentryitems = (from p in entry_items
                                                      join s in services on p.Service.Id equals s.Id
                                                      select p).ToList();
            return service_filteredentryitems;
        }

        public List<Entry> FilterEntryByLikes(List<Entry> entry_items)
        {
            // return a list of items with at least 1 Like
            List<Entry> like_filteredentryitems = (from p in entry_items as IEnumerable<Entry>
                                                   where p.Likes > 0
                                                   select p).ToList();
            return like_filteredentryitems;
        }

        public List<Entry> FilterEntryByComments(List<Entry> entry_items)
        {
            // return a list of items with at least 1 comment
            List<Entry> comment_filteredentryitems = (from p in entry_items as IEnumerable<Entry>
                                                      where p.Comments > 0
                                                      select p).ToList();
            return comment_filteredentryitems;
        }

        public List<Entry> FilterEntryBySearchstring(List<Entry> entry_items, string searchterm)
        {
            // return a list that contains a single searchterm in the title of the item
            List<Entry> comment_filteredentryitems = (from p in entry_items as IEnumerable<Entry>
                                                      where p.Title.ToLower().Contains(searchterm.ToLower())
                                                      select p).ToList();
            return comment_filteredentryitems;
        }

        #endregion Filters

        #region Properties

        public int ItemRequestCount
        {
            get
            {
                return _maxPageCount;
            }
            set
            {
                _maxPageCount = (value - (value % PAGE_SIZE)) / PAGE_SIZE; // round off to nearest 30;
            }
        }

        public bool GetFriends
        {
            set
            {
                _getFriends = value;
            }
        }

        public string UserName
        {
            get { return _userName; }
            set { _userName = value; }
        }

        public List<Entry> UnfilteredItems
        {
            get { return _entryCache; }
            set { _entryCache = value; }
        }

        #endregion Properties

        #region Privates

        private BackgroundWorker _loadCacheWorker = new BackgroundWorker();
        private BackgroundWorker _saveCacheWorker = new BackgroundWorker();
        private const string FRIENDFEED_USER_REQUEST = "http://www.friendfeed.com/api/feed/user/{0}?callback=callback&start={1}&num={2}";
        private const string FRIENDFEED_USER_AND_FRIENDS_REQUEST = "http://www.friendfeed.com/api/feed/user/{0}/friends?callback=callback&start={1}&num={2}";
        private const string FRIENDFEED_MULTIPLE_USERS_REQUEST = "http://www.friendfeed.com/api/feed/user?nickname={0}&callback=callback&start={1}&num={2}";
        private const int PAGE_SIZE = 30; // this is the maximum number of items that friendfeed returned per request at time of authoring
        private List<Entry> _entryCache = null;
        private bool _getFriends = true;
        private const string FILENAME_SUFFIX = "timeline";
        private int _maxPageCount = 30;
        private int _currentPage = 0, _origRecordCount = 0;
        private string _userName = string.Empty;

        #endregion Privates
    }
}
